/*
 * Copyright 2011 es-planning Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jp.arctos.ursus.extension.net;

import java.io.IOException;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.Queue;

/**
 * Datagram session.
 * Handles UDP transmission
 * 
 */
class DatagramSession extends Session {

    /** Datagram channel. */
    DatagramChannel channel;

    /** Session writer. */
    DatagramSessionWriter writer;

    /** Received data context. */
    ReceivedContext receivedContext;

    /** Session handler. */
    DatagramSessionHandler sessionHandler;

    /** Session conifguration. */
    SessionConfig config;
    
    /**
     * Constructor.
     * 
     * @param service Service
     * @param channel Channel
     * @param selectorHandler Selector handler
     * @param config Configuration
     * @throws IOException
     */
    DatagramSession(Service service, DatagramChannel channel, SelectorHandler selectorHandler, SessionConfig config) throws IOException {
        super(service, selectorHandler);

        this.channel = channel;
        this.config = config;

        sessionHandler = new DatagramSessionHandler(this);
        writer = new DatagramSessionWriter();
        writer.initQueue(config.getSendQueueSize());
        receivedContext = new ReceivedContext();
        receivedContext.setBuffer(ByteBuffer.allocate(config.getReceivedContextBufferSize()));
    }

    /**
     * {@inheritDoc}
     * 
     */
    @Override
    void doClose() throws IOException {
        try {
            writer.stopWrite();
            channel.close();
        } finally {
            selectorHandler.cancel(channel);
            service.sessionClosed(this);
        }
    }

    /**
     * Get the session handler for this session.
     * 
     * @return Session handler
     */
    DatagramSessionHandler getSessionHandler() {
        return sessionHandler;
    }

    /**
     * Connect to remote host.
     * 
     * @param remoteAddress Remote host address
     * @throws IOException
     * @see java.nio.DatagramChannel#connect(SocketAddress)
     */
    void connect(SocketAddress remoteAddress) throws IOException {
        channel.connect(remoteAddress);
    }

    /**
     * Disconnect from remote host.
     * 
     * @throws IOException
     * @see java.nio.DatagramChannel#disconnect()
     */
    void disconnect() throws IOException {
        channel.disconnect();
    }

    /**
     * Get status of connection to remote host.
     * 
     * @return "true" if connected, "false" if closed or not connected
     */
    boolean isConnected() {
        return channel.isOpen() && channel.isConnected();
    }

    /**
     * Receive data from channel.
     * One instance is reused for returning received data context, so it is valid until the next call.
     * 
     * @return Received data context
     * @throws IOException
     */
    ReceivedContext read() throws IOException {
        ByteBuffer buffer = receivedContext.getBuffer();
        buffer.clear();

        SocketAddress remoteAddress = channel.receive(buffer);
        if (remoteAddress != null) {
            buffer.flip();
            receivedContext.setRemoteAddress(remoteAddress);
            receivedContext.setLocalAddress(channel.socket().getLocalSocketAddress());
            return receivedContext;
        }

        return null;
    }

    /**
     * Get send request queue.
     * 
     * @return Send request queue
     */
    Queue<SendRequest> getWriteQueue() {
        return writer.getQueue();
    }

    /**
     * Accept send requests.
     * 
     * @param request message to send
     * @return "true" if accepted, "false" if unable to be accepted
     */
    boolean send(SendRequest request) {
        if (isClosed()) {
            throw new IllegalStateException();
        }

        return writer.write(selectorHandler, channel, request);
    }

    /**
     * Write send request to channel.
     * 
     * @return Send request data that has been successfully sent
     * @throws Exception
     */
    SendRequest write() throws Exception {
        return writer.onWritable(selectorHandler, channel);
    }

    /**
     * {@inheritDoc}
     * 
     */
    @Override
    SocketAddress getLocalAddress() {
        return channel.socket().getLocalSocketAddress();
    }

    /**
     * Get address of connected remote host.
     * 
     * @return Remote host address
     */
    SocketAddress getRemoteAddress() {
        return channel.socket().getRemoteSocketAddress();
    }

    /**
     * Get value of SO_RCVBUF socket option.
     * 
     * @return SO_RCVBUF Socket option
     * @throws SocketException 
     * @see java.net.DatagramSocket#getReceiveBufferSize()
     */
    int getReceiveBufferSize() throws SocketException {
        return channel.socket().getReceiveBufferSize();
    }

    /**
     * Get value of SO_REUSEADDR socket option.
     * 
     * @return SO_REUSEADDR Socket option
     * @throws SocketException 
     * @see java.net.DatagramSocket#getReuseAddress()
     */
    boolean getReuseAddress() throws SocketException {
        return channel.socket().getReuseAddress();
    }

    /**
     * Get value of SO_TIMEOUT socket option.
     * 
     * @return SO_TIMEOUT Socket optin
     * @throws SocketException 
     * @see java.net.DatagramSocket#getSoTimeout()
     */
    int getSoTimeout() throws SocketException {
        return channel.socket().getSoTimeout();
    }

    /**
     * Configure SO_TIMEOUT socket option.
     * 
     * @param timeout SO_TIMEOUT Socket option
     * @throws SocketException 
     * @see java.net.DatagramSocket#setSoTimeout(int)
     */
    void setSoTimeout(int timeout) throws SocketException {
        channel.socket().setSoTimeout(timeout);
    }

    /**
     * Get value of SO_BROADCAST socket option.
     * 
     * @return SO_BROADCAST Socket option
     * @throws SocketException 
     * @see java.net.DatagramSocket#getBroadcast()
     */
    boolean getBroadCast() throws SocketException {
        return channel.socket().getBroadcast();
    }

    /**
     * Configure SO_BROADCAST socket option.
     * 
     * @param broadCast SO_BROADCAST Socket option
     * @throws SocketException
     * @see java.net.DatagramSocket#setBroadcast(boolean)
     */
    void setBroadCast(boolean broadCast) throws SocketException {
        channel.socket().setBroadcast(broadCast);
    }

    /**
     * Get value of SO_SNDBUF socket option.
     * 
     * @return SO_SNDBUF Socket option
     * @throws SocketException 
     * @see java.net.DatagramSocket#getSendBufferSize()
     */
    int getSendBufferSize() throws SocketException {
        return channel.socket().getSendBufferSize();
    }

    /**
     * Configure SO_SNDBUF socket option.
     * 
     * @param size SO_SNDBUF Socket option
     * @throws SocketException 
     * @see java.net.DatagramSocket#setSendBufferSize(int)
     */
    void setSendBufferSize(int size) throws SocketException {
        channel.socket().setSendBufferSize(size);
    }

    /**
     * Get traffic class.
     * 
     * @return Traffic class
     * @throws SocketException
     * @see java.net.DatagramSocket#getTrafficClass() 
     */
    int getTrafficClass() throws SocketException {
        return channel.socket().getTrafficClass();
    }

    /**
     * Configure traffic class.
     * 
     * @param trafficClass Traffic class
     * @throws SocketException
     * @see java.net.DatagramSocket#setTrafficClass(int)
     */
    void setTrafficClass(int trafficClass) throws SocketException {
        channel.socket().setTrafficClass(trafficClass);
    }
}
